/**
 * MC Validation
 * 
 * A flexible Javascript class for validating form data.
 *
 * @author Sean Murphy <sean.mcval@mindcomet.com>
 * @link http://mindcomet.com
 * @version 1.0
 * @todo Write more validation rules.
 */

/**
 * MC_Validation constructor
 *
 * @param object formObj The form to be validated.
 * @param string msgType [optional] Set the message display type to 'alert' or 'inline'.
 * @return object Returns a MC_Validation object
 */
function MC_Validation(formObj, msgType) {
	this.form = formObj;
	this.rules = [];
	this.badFields = [];
	this.show_msgs = true;
	this.CSS_error = '';
	this.msg_area = '';
	
	if (msgType == 'inline') {
		this.msg_type = msgType;
	} else {
		this.msg_type = 'alert';
	}
}

/**
 * setClass
 *
 * Sets the CSS class name to be applied to message <div>s and form fields.
 *
 * @param string error The CSS class name.
 * @return void
 */
MC_Validation.prototype.setClass = function(error) {
	this.CSS_error = error;
}

/**
 * setMsgArea
 *
 * Sets the HTML container element error messages will be placed in.
 * 
 * By creating our own div inside the element that is passed, we are afforded
 * better styling control and won't disrupt any other messages which might be
 * there already from a server side script.
 *
 * @param string elm_id The id of the element.
 * @return void
 */
MC_Validation.prototype.setMsgArea = function(elm_id) {
	var wrapper = document.getElementById(elm_id);
	
	this.msg_area = document.createElement('div');
	this.msg_area.setAttribute('class', 'msgs');
	
	wrapper.appendChild(this.msg_area);
}

/**
 * setShowMsgs
 *
 * Sets whether or not we should display error messages when validation fails.
 *
 * @param bool value
 * @return void
 */
MC_Validation.prototype.setShowMsgs = function(value) {
	this.show_msgs = (value) ? true:false;
}

/**
 * addRules
 *
 * Adds a validation rule for a form element.
 * 
 * @param string id The id of the element having a rule applied to it.
 * @param string rules Pipe (|) seperated list of rule function names. Optionally you can specify a custom callback function.
 * @param string msg [optional] A custom error message to be displayed when this rule fails.
 * 								If you pass 'false' as the third paramiter no error message will be displayed.
 * @return void
 */
MC_Validation.prototype.addRules = function(id, rules, msg) {
	if (document.getElementById(id) && rules) {
		this.rules.push(new Array(document.getElementById(id), rules, msg));
	}
}

/**
 * _addClassName
 *
 * Adds a CSS class to an element without disrupting other classes set to it.
 *
 * @param string element The element having the class applied to it.
 * @param string class_name Name of the CSS class.
 * @return void
 */
MC_Validation.prototype._addClassName = function(element, class_name) {
	var regex = new RegExp(class_name);
	if (!element.className.match(regex)) {
		var classes = element.className.split(' ');
		classes.push(class_name);
		element.className = classes.join(' ');
	}
}

/**
 * _removeClassName
 *
 * Removes a CSS class from an element without disrupting other classes set to it.
 *
 * @param string element The element having the class removed from it.
 * @param string class_name Name of the CSS class.
 * @return void
 */
MC_Validation.prototype._removeClassName = function(element, class_name) {
	var regex = new RegExp(class_name);
	element.className = element.className.replace(regex, '');
}

/**
 * clearErrors
 *
 * Clear previously highlighted fields and error messages
 *
 * @return void
 */
MC_Validation.prototype.clearErrors = function() {
	for(var i = 0; i < this.form.length; i++) {
		this._removeClassName(this.form.elements[i], this.CSS_error);
	}
	
	this.msg_area.innerHTML = '';
}

/**
 * displayErrors
 *
 * Handles error message display and form field highlighting
 *
 * @note If you would like to prevent form fields from being highlighted
 *		 use setClasses('')
 * @return void
 */
MC_Validation.prototype.displayErrors = function() {
	var msgs = '';
	
	for(var x = 0; x < this.badFields.length; x++) {
		// Highlight fields with errors
		this._addClassName(this.badFields[x][0], this.CSS_error);
		
		// Queue up our error messages
		if (this.badFields[x][1] != 'false') {
			if (this.msg_type == 'inline') {
				var onClick = 'onClick="document.getElementById(\'' + this.badFields[x][0].id + '\').focus();"';
				msgs += '<div class="' + this.CSS_error + '" ' + onClick + '>' + this.badFields[x][1] + "</div>\n";
			} else {
				msgs += this.badFields[x][1] + '\n';
			}
		}
	}
	
	if (this.show_msgs) {
		switch(this.msg_type) {
			case 'inline':
				if (!this.msg_area)
					alert('Your message display type is set to inline, but you have not set a valid message area using setMsgArea().');
				else
					this.msg_area.innerHTML = msgs;
				break;
			case 'alert':
				alert(msgs);
				break;
		}
	}
	
	// Set focus to first field with error
	this.badFields[0][0].focus();
}

/**
 * validate
 *
 * Runs the current form data through all the validation rules that were set.
 *
 * @return bool True if all rules passed, false (and calls displayErrors()) if rules failed
 * @todo Could use more inline documentation.
 */
MC_Validation.prototype.validate = function() {
	// Reset our array of failed rules
	this.badFields = [];
	
	var element;
	var mrules;
	var error;
	var errorMsgs;
	var params;
	var rule_parts;
	
	for(var idx = 0; idx < this.rules.length; idx++) {
		// Reset array of error messages
		errorMsgs = [];
		
		element = this.rules[idx][0];
		
		// For cases when multiple rules are passed at one time.
		mrules = this.rules[idx][1].split('|');
		
		for(var rule = 0; rule < mrules.length; rule++) {
			rule_parts = mrules[rule].match(/^(\w+)(\[(\w+)\])?$/);
			
			if (!eval('this.' + rule_parts[1]) && eval(rule_parts[1])) {
				// Custom callback
				params = 'element';
			} else {
				// Some rules will need access to attributes other than element.value;
				// we do that here.
				switch(mrules[rule]) {
					case 'optionSelected':
						params = 'element.selectedIndex';
						break;
					case 'radioSelected':
						params = 'element.name';
						break;
					case 'checkboxSelected':
						params = 'element.checked';
						break;
					default:
						params = 'element.value';
				}

				rule_parts[1] = 'this.' + rule_parts[1];
			}
			
			if (rule_parts[3]) {
				params += ", '" + rule_parts[3] + "'";
			}
			
			error = eval(rule_parts[1] + '(' + params + ')');
			
			if (error) {
				errorMsgs.push(error);
			}
		}
		
		if(errorMsgs.length > 0) {
			// Check if a custom message was set for this field.
			if (this.rules[idx][2]) {
				errorMsgs[0] = this.rules[idx][2];
			}
			this.badFields.push(new Array(element, errorMsgs[0]));
		}
	}

	this.clearErrors();

	if(this.badFields.length > 0) {
		this.displayErrors();
		return(false);
	} else {
		this.msg_area.parentNode.removeChild(this.msg_area);
		return(true);
	}
}

/**
 * validEmail
 *
 * Validates email addresses according to RFC 2822 (and some common usage exceptions).
 *
 * @param string str The email address.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.validEmail = function(str) {
	var error = "Invalid email address.";
	
	if (!str)
		return '';
	
	// Check for invalid characters
	if (str.match(/[\x00-\x1F\x7F-\xFF]/))
		return error;

	// Check that there's one @ symbol, and that the lengths are right
	if (!str.match(/^[^@]{1,64}@[^@]{1,255}$/))
		return error;

	// Split it into sections to make life easier
	var email_array = str.split('@');

	// Check local part
	var local_array = email_array[0].split('.');
	for(var i = 0; i < local_array.length; i++) {
		if (!local_array[i].match(/^(([A-Za-z0-9!#$%&\'*+\/=?^_`{|}~-]+)|("[^"]+"))$/))
			return error;
	}

	// Check domain part
	if (!email_array[1].match(/^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}$/) ||
		!email_array[1].match(/^\[(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]$/)) {
		// If not an IP address
		
		var domain_array = email_array[1].split('.');
		if (domain_array.length < 2) { // Not enough parts to be a valid domain
			return error;
		}

		for(var j = 0; j < domain_array.length; j++) {
			if (!domain_array[j].match(/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/))
				return error;
		}
	}
	
	return '';
}

/**
 * validPhone
 *
 * Validates a phone number regardless of valid delimiters used.
 * Valid delimiters are spaces ( ), dashes (-), periods (.), and pluses (+).
 * Supports country codes.
 *
 * @param string str The phone number.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.validPhone = function(str) {
	/*
	| country_code = "(\+?\d{1,3})?";
	| area_code = "(\(\d{3}\)|\d{3})";
	| exchange = "\d{3}";
	| local = "\d{4}";
	| delimiter = "(\.|\s|-)?";
	| pattern = '/^'+country_code+delimiter+area_code+delimiter+exchange+delimiter+local+'$/';
	*/
	
	if (str && !str.match(/^(\+?\d{1,3})?(\.|\s|-)?(\(\d{3}\)|\d{3})(\.|\s|-)?\d{3}(\.|\s|-)?\d{4}$/)) {
		return 'Invalid phone number.';
	}
	return '';
}

/**
 * formatPhone
 *
 * Formats phone number in a standard way. (Consistancy man, consistancy)
 * Returns phone number in the format of +123.123.123.1234 or 123.123.1234
 *
 * @param string str The unformated phone number
 * @return string Formatted phone number.
 */
MC_Validation.prototype.formatPhone = function(str) {
	var clean = str.replace(/[^0-9]/, '');

	if (clean.length > 10) {
		return clean.replace(/^(\d{1,3})(\d{3})(\d{3})(\d{4})$/, '+$1.$2.$3.$4');
	}
	return clean.replace(/^(\d{3})(\d{3})(\d{4})$/, '$1.$2.$3');
}

/**
 * validZip
 *
 * Validates zip codes. Accepts the forms 12345 and 12345-1234.
 *
 * @param string str The zip code.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.validZip = function(str) {
	if (str && !str.match(/^\d{5}(-\d{4})?$/)) {
		return 'Invalid zip code.';
	}
	return '';
}

/**
 * isNotEmpty
 *
 * Checks if a string is NOT empty.
 *
 * @param string str The string.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.isNotEmpty = function(str) {
	if (str.length === 0) {
		return 'Field has not been filled in.';
	}
	return '';	  
}

/**
 * isEmpty
 *
 * Checks if a string IS empty.
 *
 * @param string str The string.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.isEmpty = function(str) {
	if (str.length !== 0) {
		return 'Field should not be filled in.';
	}
	return '';	  
}

/**
 * alnum
 *
 * Checks if a string alphanumeric (including underscore).
 * @param string str The string.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.alnum = function(str) {
	if (str && !str.match(/^\w+$/)) {
		return 'Field should contain only alphanumeric characters.';
	}
	return '';
}

/**
 * alpha
 *
 * Checks if a string alphabetic.
 *
 * @param string str The string.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.alpha = function(str) {
	if (str && !str.match(/^[a-zA-Z]+$/)) {
		return 'Field should contain only alphabetic characters.';
	}
	return '';
}

/**
 * digits
 *
 * Checks if string contains only digits.
 *
 * @param string str The string.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.digits = function(str) {
	if (str && !str.match(/^\d+$/)) {
		return 'Field should contain only digits (0-9).';
	}
	return '';
}

/**
 * min_length
 *
 * Checks if string is of a minimum length.
 *
 * @param string str The string.
 * @param string len The minimum length.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.min_length = function(str, len) {
	if (str.length < len) {
		return 'Field must be at least ' + len + ' characters long.';
	}
	return '';
}

/**
 * max_length
 *
 * Checks if string is of a maximum length.
 *
 * @param string str The string.
 * @param string len The maximum length.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.max_length = function(str, len) {
	if (str.length > len) {
		return 'Field must be less than ' + len + ' characters long.';
	}
	return '';
}

/**
 * exact_length
 *
 * Checks if string is of an exact length.
 *
 * @param string str The string.
 * @param string len The exact length.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.exact_length = function(str, len) {
	if (str.length != len) {
		return 'Field must be ' + len + ' characters long.';
	}
	return '';
}

/**
 * matches
 *
 * Checks if string matches another string.
 *
 * @param string str The first string.
 * @param string id The id of the element to match.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.matches = function(str, id) {
	if (str != document.getElementById(id).value) {
		return 'Fields do not match.';
	}
	return '';
}

/**
 * optionSelected
 *
 * Checks if select box has option selected.
 *
 * @param string index The selectedIndex.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.optionSelected = function(index) {
	if (!index) {
		return 'You must make a selection.';
	}
	return '';
}

/**
 * radioSelected
 *
 * Checks if one radio in a radio group has be selected.
 *
 * @param string grpName The name attribute radios share in common.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.radioSelected = function(grpName) {
	var radios = eval('this.form.' + grpName);
	
	for(var n = 0; n < radios.length; n++) {
		if (radios[n].checked)
			return '';
	}
	
	return 'You must select a radio button.';
}

/**
 * checkboxSelected
 *
 * Checks if checkbox has been checked.
 *
 * @param string checked The checked attribute of a checkbox.
 * @return string Empty string if success, else an error message.
 */
MC_Validation.prototype.checkboxSelected = function(checked) {
	if (!checked) {
		return 'Checkbox must be checked.';
	}
	return '';
}